<?php
/**
* Base of database rows
* 
* @package Smernel
*/
	class _smuoiDbRow extends _smuoiDbObj{
		
		/**
		* The table name must be set in any class that extends this
		* @var string
		*/
		protected $table;
			
####	the following may be redefined as needed

		/**
		* special keys that trigger a set function, which must of course be provided and prefixed with "set_" (e.g. set_file_contents).
		*
		* Should be listed in the desired order (if any) in which they should be called on commit. 
		* May be null, single string value, comma seperated values or array
		* a set function *must* return true on success (or else edit will be unset)
		* @var mixed
		*/
		protected $set_functions;
		
		/**
		* special keys that trigger a get function, which need no prefix (e.g. file_contents(){ ... )   
		*
		* May be null, single string value, comma seperated values or array
		* @var mixed
		*/
		protected $get_functions;
		
		/**
		* db columns that may not be updated on commit. null, single string value, comma seperated values or array 
		*
		* May be null, single string value, comma seperated values or array
		* @var mixed
		*/
		protected $read_only_keys = 'id';
	
####	The following are for use by this element
		
		/**
		* Unique database id of this element
		*
		* @internal
		* @var string
		*/
		protected $id;
		
		/**
		* The data
		*
		* @internal
		* @var object
		*/
		protected $data;
		
		/**
		* Package this element belongs to
		*
		* @internal
		* @var _smuoiPackage
		*/
		public $pkg;
		
		/**
		* actions to perform on commit
		*
		* @internal
		* @var array
		*/ 
		protected $edits;
		
		/**
		* things to check through on commit
		*
		* @internal
		* @var array
		*/ 
		protected $sanity_checks;
		
		/**
		* whether all columns have been retrieved
		*
		* @internal
		* @deprecated
		* @var bool
		*/ 
		public $resolved;
		
		/**
		* whether is new element
		*
		* @internal
		* @var bool
		*/ 
		protected $new = false;
		
		/**
		* classname, minus the namespace prefix
		*
		* @internal
		* @var string
		*/
		protected $class;
		
#### 	Methods...
	
		/**
		* constructor
		*
		* The id must be given if the element is not new, and the package object must always be given
		* @param string id
		* @param _smuoiPackage
		* @param array|object data
		* @param bool resolved
		*/
		function __construct($id, $package, $data=false, $resolved=false){
			$this->pkg = $package;
			$this->class = $this->get_class();
			$this->resolved = $resolved;
			if(!$this->id = $id) return $this->initialize($data);
			$this->data = new obj($data);
		}
	
		/**
		* initializes with given data
		*
		* @internal
		* @param array|object data
		*/
		protected function initialize($data){
			$this->new = true;
			$o = new obj($data);
			if(!$o->get('id')) $o->id = $this->pkg->new_id($this->key());
			$this->id = $o->id;
			$o->class = $this->class;
			$this->data = $o;
		}
		
		/**
		* classname of element without namespce prefix
		* @returns string
		*/
		function get_class(){
			return $this->pkg->remove_namespace(get_class($this));
		}

####	Set
	
		/**
		* sets data values
		* 
		* Objects/arrays passed as first value are forced into the data object
		* @param mixed key|object|array
		* @param mixed value
		*/
		function set($k, $v=null){
			if(!is_scalar($k)) return $this->data->set($k);
			(isset($this->data->$k) || $this->is_set_function($k)) ? $this->edits[$k] = $v : $this->data->$k = $v;
		}
		
		/**
		* null completes half-retrieved objects whilst not overwriting any changes with old data
		* @param object|array data
		*/
		function set_if_null($o){
			foreach($o as $k=>$v) if(!isset($this->data->$k)) $this->data->$k = $v;
		}
		
		/**
		* sets data and edits simultaneously
		* @internal
		* @param string key
		* @param mixed value
		*/
		protected function hard_set($k, $v){
			return $this->edits[$k] = $this->data->$k = $v;	
		}
		
####	Get

		/**
		* gets data from data object or get function
		* @param string key
		* @returns mixed
		*/
		function get($k){
			if(isset($this->data->$k)) return $this->data->$k;
			if($this->is_get_function($k)) return $this->$k();
		}
		
####	Data object clone
		
		/**
		* clones data object for inspection
		* @returns object
		*/
		function data(){
			return new obj($this->data);
		}

####	Quick and dirty shortcut to the dataProxy

		/**
		* Quick and dirty shortcut to the dataProxy
		* 
		* give element id to retrieve it, omit to retrieve dataProxy
		* @param id of element to retrieve
		* @returns mixed
		*/
		function db($id=NULL){
			return (!is_null($id)) ? $this->pkg->db->get($id) : $this->pkg->db;
		}
		
####	Table, id key and valid columns
	
		/**
		* Real name of table this element belongs to
		* @returns string
		*/
		function table(){
			return $this->pkg->table($this->table);
		}
	
		/**
		* Key of this element's table
		* @returns string
		*/
		function key(){
			return $this->pkg->key($this->table);
		}
	
		/**
		* Valid columns on this emelemnt's table
		* @returns array
		*/
		function cols(){
			return $this->pkg->cols($this->table);
		}
		
####	Settings arrays

		/**
		* gets internal settings array
		* @internal
		* @param string name
		* @returns array
		*/
		protected function get_settings_array($a){
			if(is_null($this->$a) || ($this->$a == '*')) return array();
			if(!is_array($this->$a)) $this->$a = explode(',', $this->$a);
			return $this->$a;
		}
		
		/**
		* detemrmines if key is in settings array
		* @internal
		* @param string key
		* @param string name
		* @param bool wildcards allowed
		* @returns bool
		*/
		protected function in_settings_array($n, $a, $w=false){
			if(is_null($this->$a)) return false;
			if(is_string($this->$a)){
				if($w && ($this->$a == '*')) return true;
				$this->$a = explode(',', $this->$a);
			}
			return in_array($n, $this->$a);
		}
		
		/**
		* read only keys of this element
		* @returns array
		*/
		function read_only_keys(){
			return $this->get_settings_array('read_only_keys');
		}
		
		/**
		* get functions of this element
		* @returns array
		*/
		function get_functions(){
			return $this->get_settings_array('get_functions');
		}
		
		/**
		* set functions of this element
		* @returns array
		*/
		function set_functions(){
			return $this->get_settings_array('set_functions');
		}
		
		/**
		* gcolumns that may be updated
		* @returns array
		*/
		function updateable_cols(){
			$cols = array();
			foreach($this->cols() as $col) if(!$this->is_read_only_key($col)) $cols[] = $col;
			return $cols;
		}
		
		/**
		* not sure what this was about
		* @deprecated
		*/
		function inherit_settings_array(){
			return array();	
		}
		
####	Bool functions
	
		/**
		* whether a key belongs in the data object
		* @param string key
		* @returns bool
		*/
		function is_valid_data_key($k){
			return in_array($k, $this->cols());
		}
		
		/**
		* whether a key is read-only
		* @param string key
		* @returns bool
		*/
		function is_read_only_key($k){
			return $this->in_settings_array($k, 'read_only_keys');
		}
		
		/**
		* whether a key is a get function
		* @param string key
		* @returns bool
		*/
		function is_get_function($k){
			return $this->in_settings_array($k, 'get_functions');
		}
		
		/**
		* whether a key is a set function
		* @param string key
		* @returns bool
		*/
		function is_set_function($k){
			return $this->in_settings_array($k, 'set_functions');
		}
		
		/**
		* whether a key is an updateable column
		* @param string key
		* @returns bool
		*/
		function is_updateable($k){
			return ($this->is_valid_data_key($k) && !$this->is_read_only_key($k));
		}
		
		/**
		* whether a key is settable
		* @param string key
		* @returns bool
		*/
		function is_settable($k){
			return ( !$this->is_read_only_key($k) ) && ( $this->is_set_function($k) || $this->is_valid_data_key($k) );
		}

####	Commital

		/**
		* pre-commit procedure. Commital aborted if fails to return true
		* @returns bool
		*/
		protected function prepare_commit(){
			if(!$this->new) $this->run_set_functions();
			$this->run_sanity_checks();
			return true;
		}
	
		/**
		* Commital of edits
		*
		* $nq is relevant to newly created rows, and when true the db is written to immediately rather than on on exit (usually the data joins an insert queue)
		* @param bool skip insert queue
		* @returns bool success
		*/
		function commit($nq=false){
			if(!$this->prepare_commit()) return false;
			if(!$this->new){
				if(!$e = $this->get_edits()) return true;
				if(!$this->pkg->al()->update($this->table(), $e, array('id'=>$this->id))) return false;
			}else{
				$e = $this->get_new();
				if(!$this->pkg->al()->insert($this->table(), $e, !(bool)$nq)) return false;
				$this->new = false;
			}
			$this->edits = null;
			$this->data->set($e);
			$this->db()->set($this->id, $this);
			return true;
		}
	
####	Sanity check

		/**
		* Adds item to check to sanity check list
		* @param string check name
		*/
		function add_sanity_check($c){
			$this->sanity_checks[$c] = $c;	
		}
		
		/**
		* Sanity check procedure
		* @internal
		*/
		protected function run_sanity_checks(){
			if(!is_array($this->sanity_checks)) return;
			while($c = current($this->sanity_checks)){
				$this->check($c);
				next($this->sanity_checks);
			}
		}

		/**
		* Checks a sanity check item (for overwriting by children)
		*/
		protected function check(){}
			
####	Set functions

		/**
		* Set function procedure
		* @internal
		*/
		protected function run_set_functions(){
			if(!$this->set_functions || !$this->edits) return;
			foreach($this->set_functions() as $k){
				if(isset($this->edits[$k])){
					// Set functions must return true on success
					if(!$this->run_set_function($k, $this->edits[$k])) unset($this->edits[$k]);
				}
			}
		}
		
		/**
		* Runs a set function
		* @internal
		*/
		protected function run_set_function($func, $arg){
			$func = 'set_'.$func;
			return $this->$func($arg);
		}
	
####	Edits array
		
		/**
		* Gets the intended value of a key
		* @param string key
		* @returns mixed
		*/
		protected function get_intended($k){
			if(isset($this->edits[$k])) return $this->edits[$k];
			if(isset($this->data->$k)) return $this->data->$k;
		}
		
		/**
		* Whether a key was edited
		* @param string key
		* @returns bool
		*/
		protected function is_edited($k){
			return (isset($this->edits[$k]) && ($this->edits[$k] !== $this->data->$k));
		}
		
		/**
		* Gets all column keys for a new element
		* @internal
		* @returns object
		*/
		protected function get_new(){
			$o = new obj;
			foreach($this->cols() as $k) if(!is_null($v = $this->get_intended($k))) $o->$k = $v;
			return $o;
		}
		
		/**
		* Gets all edited column keys for an update commital
		* @internal
		* @returns object
		*/
		protected function get_edits(){
			if(is_null($this->edits)) return false;
			$o = new jso;
			foreach($this->updateable_cols() as $k) if(isset($this->edits[$k])) $o->$k = (string) $this->edits[$k];
			if(!$o->length()) return false;
			return $o;
		}

####	DB Resolution

		/**
		* Bool all columns were retrieved from database
		* @deprecated
		*/
		function resolved(){
			if($this->resolved) return true;
			foreach($this->cols() as $k) if(!isset($this->data->$k)) return $this->resolved = false;
			return $this->resolved = true;
		}

		/**
		* Resolves object
		* @deprecated
		*/
		function resolve(){
			if($this->resolved()) return;
			if($this->new) $this->commit(true);
			// We must clear data so as not to overwrite edits
			$this->data->clear();
			$this->db()->retrieve($this->id, $this->table());
			$this->resolved = true;
		}
	
#### 	Magic :)
	
		function __set($k, $v){
			return $this->set($k, $v);
		}
	
		function __get($k){
			return $this->get($k);
		}
	
		function __isset($k){
			return (isset($this->data->$k)||($this->is_get_function($k)));
		}
	
		function __unset($k){}		
	
		function __tostring(){
			return $this->id;
		}		
	}

